local TOML = {
    -- denotes the current supported TOML version
    version = 0.40,

    -- sets whether the parser should follow the TOML spec strictly
    -- currently, no errors are thrown for the following rules if strictness is turned off:
    --   tables having mixed keys
    --   redefining a table
    --   redefining a key within a table
    strict = true,
}

-- converts TOML data into a lua table
TOML.parse = function(toml, options)
    options = options or {}
    local strict = (options.strict ~= nil and options.strict or TOML.strict)

    -- the official TOML definition of whitespace
    local ws = "[\009\032]"

    -- the official TOML definition of newline
    local nl = "[\10"
    do
        local crlf = "\13\10"
        nl = nl .. crlf
    end
    nl = nl .. "]"

    -- stores text data
    local buffer = ""

    -- the current location within the string to parse
    local cursor = 1

    -- the output table
    local out = {}

    -- the current table to write to
    local obj = out

    -- returns the next n characters from the current position
    local function char(n)
        n = n or 0
        return toml:sub(cursor + n, cursor + n)
    end

    -- moves the current position forward n (default: 1) characters
    local function step(n)
        n = n or 1
        cursor = cursor + n
    end

    -- move forward until the next non-whitespace character
    local function skipWhitespace()
        while(char():match(ws)) do
            step()
        end
    end

    -- remove the (Lua) whitespace at the beginning and end of a string
    local function trim(str)
        return str:gsub("^%s*(.-)%s*$", "%1")
    end

    -- divide a string into a table around a delimiter
    local function split(str, delim)
        if str == "" then return {} end
        local result = {}
        local append = delim
        if delim:match("%%") then
            append = delim:gsub("%%", "")
        end
        for match in (str .. append):gmatch("(.-)" .. delim) do
            table.insert(result, match)
        end
        return result
    end

    -- produce a parsing error message
    -- the error contains the line number of the current position
    local function err(message, strictOnly)
        if not strictOnly or (strictOnly and strict) then
            local line = 1
            local c = 0
            for l in toml:gmatch("(.-)" .. nl) do
                c = c + l:len()
                if c >= cursor then
                    break
                end
                line = line + 1
            end
            error("TOML: " .. message .. " on line " .. line .. ".", 4)
        end
    end

    -- prevent infinite loops by checking whether the cursor is
    -- at the end of the document or not
    local function bounds()
        return cursor <= toml:len()
    end

    local function parseString()
        local quoteType = char() -- should be single or double quote

        -- this is a multiline string if the next 2 characters match
        local multiline = (char(1) == char(2) and char(1) == char())

        -- buffer to hold the string
        local str = ""

        -- skip the quotes
        step(multiline and 3 or 1)

        while(bounds()) do
            if multiline and char():match(nl) and str == "" then
                -- skip line break line at the beginning of multiline string
                step()
            end

            -- keep going until we encounter the quote character again
            if char() == quoteType then
                if multiline then
                    if char(1) == char(2) and char(1) == quoteType then
                        step(3)
                        break
                    end
                else
                    step()
                    break
                end
            end

            if char():match(nl) and not multiline then
                err("Single-line string cannot contain line break")
            end

            -- if we're in a double-quoted string, watch for escape characters!
            if quoteType == '"' and char() == "\\" then
                if multiline and char(1):match(nl) then
                    -- skip until first non-whitespace character
                    step(1) -- go past the line break
                    while(bounds()) do
                        if not char():match(ws) and not char():match(nl) then
                            break
                        end
                        step()
                    end
                else
                    -- all available escape characters
                    local escape = {
                        b = "\b",
                        t = "\t",
                        n = "\n",
                        f = "\f",
                        r = "\r",
                        ['"'] = '"',
                        ["\\"] = "\\",
                    }
                    -- utf function from http://stackoverflow.com/a/26071044
                    -- converts \uXXX into actual unicode
                    local function utf(char)
                        local bytemarkers = {{0x7ff, 192}, {0xffff, 224}, {0x1fffff, 240}}
                        if char < 128 then return string.char(char) end
                        local charbytes = {}
                        for bytes, vals in pairs(bytemarkers) do
                            if char <= vals[1] then
                                for b = bytes + 1, 2, -1 do
                                    local mod = char % 64
                                    char = (char - mod) / 64
                                    charbytes[b] = string.char(128 + mod)
                                end
                                charbytes[1] = string.char(vals[2] + char)
                                break
                            end
                        end
                        return table.concat(charbytes)
                    end

                    if escape[char(1)] then
                        -- normal escape
                        str = str .. escape[char(1)]
                        step(2) -- go past backslash and the character
                    elseif char(1) == "u" then
                        -- utf-16
                        step()
                        local uni = char(1) .. char(2) .. char(3) .. char(4)
                        step(5)
                        uni = tonumber(uni, 16)
                        if (uni >= 0 and uni <= 0xd7ff) and not (uni >= 0xe000 and uni <= 0x10ffff) then
                            str = str .. utf(uni)
                        else
                            err("Unicode escape is not a Unicode scalar")
                        end
                    elseif char(1) == "U" then
                        -- utf-32
                        step()
                        local uni = char(1) .. char(2) .. char(3) .. char(4) .. char(5) .. char(6) .. char(7) .. char(8)
                        step(9)
                        uni = tonumber(uni, 16)
                        if (uni >= 0 and uni <= 0xd7ff) and not (uni >= 0xe000 and uni <= 0x10ffff) then
                            str = str .. utf(uni)
                        else
                            err("Unicode escape is not a Unicode scalar")
                        end
                    else
                        err("Invalid escape")
                    end
                end
            else
                -- if we're not in a double-quoted string, just append it to our buffer raw and keep going
                str = str .. char()
                step()
            end
        end

        return {value = str, type = "string"}
    end

    local function parseNumber()
        local num = ""
        local exp
        local date = false
        while(bounds()) do
            if char():match("[%+%-%.eE_0-9]") then
                if not exp then
                    if char():lower() == "e" then
                        -- as soon as we reach e or E, start appending to exponent buffer instead of
                        -- number buffer
                        exp = ""
                    elseif char() ~= "_" then
                        num = num .. char()
                    end
                elseif char():match("[%+%-0-9]") then
                    exp = exp .. char()
                else
                    err("Invalid exponent")
                end
            elseif char():match(ws) or char() == "#" or char():match(nl) or char() == "," or char() == "]" or char() == "}" then
                break
            elseif char() == "T" or char() == "Z" then
                -- parse the date (as a string, since lua has no date object)
                date = true
                while(bounds()) do
                    if char() == "," or char() == "]" or char() == "#" or char():match(nl) or char():match(ws) then
                        break
                    end
                    num = num .. char()
                    step()
                end
            else
                err("Invalid number")
            end
            step()
        end

        if date then
            return {value = num, type = "date"}
        end

        local float = false
        if num:match("%.") then float = true end

        exp = exp and tonumber(exp) or 0
        num = tonumber(num)

        if not float then
            return {
                -- lua will automatically convert the result
                -- of a power operation to a float, so we have
                -- to convert it back to an int with math.floor
                value = math.floor(num * 10^exp),
                type = "int",
            }
        end

        return {value = num * 10^exp, type = "float"}
    end

    local parseArray, getValue
    
    function parseArray()
        step() -- skip [
        skipWhitespace()

        local arrayType
        local array = {}

        while(bounds()) do
            if char() == "]" then
                break
            elseif char():match(nl) then
                -- skip
                step()
                skipWhitespace()
            elseif char() == "#" then
                while(bounds() and not char():match(nl)) do
                    step()
                end
            else
                -- get the next object in the array
                local v = getValue()
                if not v then break end

                -- set the type if it hasn't been set before
                if arrayType == nil then
                    arrayType = v.type
                elseif arrayType ~= v.type then
                    err("Mixed types in array", true)
                end

                array = array or {}
                table.insert(array, v.value)
                
                if char() == "," then
                    step()
                end
                skipWhitespace()
            end
        end
        step()

        return {value = array, type = "array"}
    end

    local function parseInlineTable()
        step() -- skip opening brace

        local buffer = ""
        local quoted = false
        local tbl = {}

        while bounds() do
            if char() == "}" then
                break
            elseif char() == "'" or char() == '"' then
                buffer = parseString().value
                quoted = true
            elseif char() == "=" then
                if not quoted then
                    buffer = trim(buffer)
                end

                step() -- skip =
                skipWhitespace()

                if char():match(nl) then
                    err("Newline in inline table")
                end

                local v = getValue().value
                tbl[buffer] = v

                skipWhitespace()

                if char() == "," then
                    step()
                elseif char():match(nl) then
                    err("Newline in inline table")
                end

                quoted = false
                buffer = ""
            else
                buffer = buffer .. char()
                step()
            end
        end
        step() -- skip closing brace

        return {value = tbl, type = "array"}
    end

    local function parseBoolean()
        local v
        if toml:sub(cursor, cursor + 3) == "true" then
            step(4)
            v = {value = true, type = "boolean"}
        elseif toml:sub(cursor, cursor + 4) == "false" then
            step(5)
            v = {value = false, type = "boolean"}
        else
            err("Invalid primitive")
        end

        skipWhitespace()
        if char() == "#" then
            while(not char():match(nl)) do
                step()
            end
        end

        return v
    end

    -- figure out the type and get the next value in the document
    function getValue()
        if char() == '"' or char() == "'" then
            return parseString()
        elseif char():match("[%+%-0-9]") then
            return parseNumber()
        elseif char() == "[" then
            return parseArray()
        elseif char() == "{" then
            return parseInlineTable()
        else
            return parseBoolean()
        end
        -- date regex (for possible future support):
        -- %d%d%d%d%-[0-1][0-9]%-[0-3][0-9]T[0-2][0-9]%:[0-6][0-9]%:[0-6][0-9][Z%:%+%-%.0-9]*
    end

    -- track whether the current key was quoted or not
    local quotedKey = false
    
    -- parse the document!
    while(cursor <= toml:len()) do

        -- skip comments and whitespace
        if char() == "#" then
            while(not char():match(nl)) do
                step()
            end
        end

        if char():match(nl) then
            -- skip
        end

        if char() == "=" then
            step()
            skipWhitespace()
            
            -- trim key name
            buffer = trim(buffer)

            if buffer:match("^[0-9]*$") and not quotedKey then
                buffer = tonumber(buffer)
            end

            if buffer == "" and not quotedKey then
                err("Empty key name")
            end

            local v = getValue()
            if v then
                -- if the key already exists in the current object, throw an error
                if obj[buffer] then
                    err('Cannot redefine key "' .. buffer .. '"', true)
                end
                obj[buffer] = v.value
            end

            -- clear the buffer
            buffer = ""
            quotedKey = false

            -- skip whitespace and comments
            skipWhitespace()
            if char() == "#" then
                while(bounds() and not char():match(nl)) do
                    step()
                end
            end

            -- if there is anything left on this line after parsing a key and its value,
            -- throw an error
            if not char():match(nl) and cursor < toml:len() then
                err("Invalid primitive")
            end
        elseif char() == "[" then
            buffer = ""
            step()
            local tableArray = false

            -- if there are two brackets in a row, it's a table array!
            if char() == "[" then
                tableArray = true
                step()
            end

            obj = out

            local function processKey(isLast)
                isLast = isLast or false
                buffer = trim(buffer)

                if not quotedKey and buffer == "" then
                    err("Empty table name")
                end

                if isLast and obj[buffer] and not tableArray and #obj[buffer] > 0 then
                    err("Cannot redefine table", true)
                end

                -- set obj to the appropriate table so we can start
                -- filling it with values!
                if tableArray then
                    -- push onto cache
                    if obj[buffer] then
                        obj = obj[buffer]
                        if isLast then
                            table.insert(obj, {})
                        end
                        obj = obj[#obj]
                    else
                        obj[buffer] = {}
                        obj = obj[buffer]
                        if isLast then
                            table.insert(obj, {})
                            obj = obj[1]
                        end
                    end
                else
                    obj[buffer] = obj[buffer] or {}
                    obj = obj[buffer]
                end
            end

            while(bounds()) do
                if char() == "]" then
                    if tableArray then
                        if char(1) ~= "]" then
                            err("Mismatching brackets")
                        else
                            step() -- skip inside bracket
                        end
                    end
                    step() -- skip outside bracket

                    processKey(true)
                    buffer = ""
                    break
                elseif char() == '"' or char() == "'" then
                    buffer = parseString().value
                    quotedKey = true
                elseif char() == "." then
                    step() -- skip period
                    processKey()
                    buffer = ""
                else
                    buffer = buffer .. char()
                    step()
                end
            end

            buffer = ""
            quotedKey = false
        elseif (char() == '"' or char() == "'") then
            -- quoted key
            buffer = parseString().value
            quotedKey = true
        end

        buffer = buffer .. (char():match(nl) and "" or char())
        step()
    end

    return out
end

TOML.encode = function(tbl)
    local toml = ""

    local cache = {}

    local function parse(tbl)
        for k, v in pairs(tbl) do
            if type(v) == "boolean" then
                toml = toml .. k .. " = " .. tostring(v) .. "\n"
            elseif type(v) == "number" then
                toml = toml .. k .. " = " .. tostring(v) .. "\n"
            elseif type(v) == "string" then
                local quote = '"'
                v = v:gsub("\\", "\\\\")

                -- if the string has any line breaks, make it multiline
                if v:match("^\n(.*)$") then
                    quote = quote:rep(3)
                    v = "\\n" .. v
                elseif v:match("\n") then
                    quote = quote:rep(3)
                end

                v = v:gsub("\b", "\\b")
                v = v:gsub("\t", "\\t")
                v = v:gsub("\f", "\\f")
                v = v:gsub("\r", "\\r")
                v = v:gsub('"', '\\"')
                v = v:gsub("/", "\\/")
                toml = toml .. k .. " = " .. quote .. v .. quote .. "\n"
            elseif type(v) == "table" then
                local array, arrayTable = true, true
                local first = {}
                for kk, vv in pairs(v) do
                    if type(kk) ~= "number" then array = false end
                    if type(vv) ~= "table" then
                        v[kk] = nil
                        first[kk] = vv
                        arrayTable = false
                    end
                end

                if array then
                    if arrayTable then
                        -- double bracket syntax go!
                        table.insert(cache, k)
                        for kk, vv in pairs(v) do
                            toml = toml .. "[[" .. table.concat(cache, ".") .. "]]\n"
                            for k3, v3 in pairs(vv) do
                                if type(v3) ~= "table" then
                                    vv[k3] = nil
                                    first[k3] = v3
                                end
                            end
                            parse(first)
                            parse(vv)
                        end
                        table.remove(cache)
                    else
                        -- plain ol boring array
                        toml = toml .. k .. " = [\n"
                        for kk, vv in pairs(first) do
                            toml = toml .. tostring(vv) .. ",\n"
                        end
                        toml = toml .. "]\n"
                    end
                else
                    -- just a key/value table, folks
                    table.insert(cache, k)
                    toml = toml .. "[" .. table.concat(cache, ".") .. "]\n"
                    parse(first)
                    parse(v)
                    table.remove(cache)
                end
            end
        end
    end
    
    parse(tbl)
    
    return toml:sub(1, -2)
end

return TOML